﻿using System;

namespace GeoCode.Core
{
    /// <summary>
    /// 
    /// </summary>
    /// <remarks></remarks>
    public struct Distance
    {
        /// <summary>
        /// 
        /// </summary>
        public const double EarthRadiusInMiles = 3956.545;
        /// <summary>
        /// 
        /// </summary>
        public const double EarthRadiusInKilometers = 6378.135;
        /// <summary>
        /// 
        /// </summary>
        private const double ConversionConstant = 0.621371192;

        /// <summary>
        /// 
        /// </summary>
        private readonly DistanceUnits _units;
        /// <summary>
        /// 
        /// </summary>
        private readonly double _value;

        /// <summary>
        /// Initializes a new instance of the <see cref="Distance"/> struct.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="units">The units.</param>
        /// <remarks></remarks>
        public Distance(double value, DistanceUnits units)
        {
            this._value = Math.Round(value, 8);
            this._units = units;
        }

        #region Helper Factory Methods

        /// <summary>
        /// Froms the miles.
        /// </summary>
        /// <param name="miles">The miles.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static Distance FromMiles(double miles)
        {
            return new Distance(miles, DistanceUnits.Miles);
        }

        /// <summary>
        /// Froms the kilometers.
        /// </summary>
        /// <param name="kilometers">The kilometers.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static Distance FromKilometers(double kilometers)
        {
            return new Distance(kilometers, DistanceUnits.Kilometers);
        }

        #endregion

        #region Unit Conversions

        /// <summary>
        /// Converts the units.
        /// </summary>
        /// <param name="units">The units.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private Distance ConvertUnits(DistanceUnits units)
        {
            if (this._units == units) return this;

            double newValue;
            switch (units)
            {
                case DistanceUnits.Miles:
                    newValue = _value*ConversionConstant;
                    break;
                case DistanceUnits.Kilometers:
                    newValue = _value/ConversionConstant;
                    break;
                default:
                    newValue = 0;
                    break;
            }

            return new Distance(newValue, units);
        }

        /// <summary>
        /// Toes the miles.
        /// </summary>
        /// <returns></returns>
        /// <remarks></remarks>
        public Distance ToMiles()
        {
            return ConvertUnits(DistanceUnits.Miles);
        }

        /// <summary>
        /// Toes the kilometers.
        /// </summary>
        /// <returns></returns>
        /// <remarks></remarks>
        public Distance ToKilometers()
        {
            return ConvertUnits(DistanceUnits.Kilometers);
        }

        #endregion

        /// <summary>
        /// Gets the value.
        /// </summary>
        /// <remarks></remarks>
        public double Value
        {
            get { return _value; }
        }

        /// <summary>
        /// Gets the units.
        /// </summary>
        /// <remarks></remarks>
        public DistanceUnits Units
        {
            get { return _units; }
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">Another object to compare to.</param>
        /// <returns><c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.</returns>
        /// <remarks></remarks>
        public override bool Equals(object obj)
        {
            return base.Equals(obj);
        }

        /// <summary>
        /// Equalses the specified obj.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public bool Equals(Distance obj)
        {
            return base.Equals(obj);
        }

        /// <summary>
        /// Equalses the specified obj.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <param name="normalizeUnits">if set to <c>true</c> [normalize units].</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public bool Equals(Distance obj, bool normalizeUnits)
        {
            if (normalizeUnits)
                obj = obj.ConvertUnits(Units);
            return Equals(obj);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
        /// <remarks></remarks>
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>A <see cref="System.String"/> that represents this instance.</returns>
        /// <remarks></remarks>
        public override string ToString()
        {
            return String.Format("{0} {1}", _value, _units);
        }

        #region Operators

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="d1">The d1.</param>
        /// <param name="d">The d.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static Distance operator *(Distance d1, double d)
        {
            double newValue = d1.Value*d;
            return new Distance(newValue, d1.Units);
        }

        /// <summary>
        /// Implements the operator +.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static Distance operator +(Distance left, Distance right)
        {
            double newValue = left.Value + right.ConvertUnits(left.Units).Value;
            return new Distance(newValue, left.Units);
        }


        /// <summary>
        /// Implements the operator -.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static Distance operator -(Distance left, Distance right)
        {
            double newValue = left.Value - right.ConvertUnits(left.Units).Value;
            return new Distance(newValue, left.Units);
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator ==(Distance left, Distance right)
        {
            return left.Equals(right);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator !=(Distance left, Distance right)
        {
            return !left.Equals(right);
        }

        /// <summary>
        /// Implements the operator &lt;.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator <(Distance left, Distance right)
        {
            return (left.Value < right.ConvertUnits(left.Units).Value);
        }

        /// <summary>
        /// Implements the operator &lt;=.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator <=(Distance left, Distance right)
        {
            return (left.Value <= right.ConvertUnits(left.Units).Value);
        }

        /// <summary>
        /// Implements the operator &gt;.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator >(Distance left, Distance right)
        {
            return (left.Value > right.ConvertUnits(left.Units).Value);
        }

        /// <summary>
        /// Implements the operator &gt;=.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator >=(Distance left, Distance right)
        {
            return (left.Value >= right.ConvertUnits(left.Units).Value);
        }

        /// <summary>
        /// Performs an implicit conversion from <see cref="GeoCode.Core.Distance"/> to <see cref="System.Double"/>.
        /// </summary>
        /// <param name="distance">The distance.</param>
        /// <returns>The result of the conversion.</returns>
        /// <remarks></remarks>
        public static implicit operator double(Distance distance)
        {
            return distance.Value;
        }

        #endregion
    }
}